﻿using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Media;
using TangoGames.Scene;

namespace TangoGames.Base

{
    public class GameObject3D:DrawableGameComponent
    {
        #region Fields

        protected Model model;

        BasicEffect lineEffect;

        protected BoundingBox modelBoundingBox;
        protected BoundingBoxBuffers modelBoundingBoxbuffers;
        protected bool drawBoundingBox = false;

        public BoundingBox ModelBoundingBox { get {return modelBoundingBox; } set {modelBoundingBox=value; } }

        public Matrix world { get; set; }
        float rotationX;
        float rotationY;
        float rotationZ;
        float scaleX;
        float scaleY;
        float scaleZ;
        float scale;

        Vector3 position;
        float positionX;
        float positionY;
        float positionZ;

        protected ICamera camera;

        //Bones Transforms
        protected Matrix[] boneTransforms;

        #endregion

        #region Constructor/Destruir

        public GameObject3D(Game game, ICamera camera, Model model):base(game)
        {
            this.camera=camera;

            this.model = model;

            // inicializa matrix do modelo
            boneTransforms = new Matrix[model.Bones.Count];

            model.CopyAbsoluteBoneTransformsTo(boneTransforms);

            //valor padrão da escala do objeto
            Scale = 1;

            //inicializa rotação
            rotationX = rotationY = rotationZ = 0;

            //inicializa posição
            Position = Vector3.Zero;
            
            this.world = Matrix.Identity;

            UpdateWorld();

            game.Components.Add(this);

            modelBoundingBox = CreateBoundingBox(model, world);

            lineEffect = new BasicEffect(game.GraphicsDevice);
            lineEffect.LightingEnabled = false;
            lineEffect.TextureEnabled = false;
            lineEffect.VertexColorEnabled = true;

            modelBoundingBoxbuffers = CreateBoundingBoxBuffers(modelBoundingBox, game.GraphicsDevice);

        }

        protected override void Dispose(bool disposing)
        {
            Game.Components.Remove(this);
            base.Dispose(disposing);
        }

        #endregion

        #region Update

        /// <summary>
        /// Update
        /// </summary>
        /// <param name="gameTime"></param>
        public override void Update(GameTime gameTime)
        {
            Update();
        }

        /// <summary>
        /// Update 
        /// </summary>
        public void Update()
        {
            UpdateWorld();
            modelBoundingBox = CreateBoundingBox(model, world);
        }

        public void UpdateWorld()
        {
            this.world =
                Matrix.CreateRotationX(rotationX) *
                Matrix.CreateRotationY(rotationY) *
                Matrix.CreateRotationZ(rotationZ) *
                Matrix.CreateScale(scaleX, scaleY, scaleZ) *
                Matrix.CreateTranslation(position);
        }

        #endregion

        #region Draw

        public override void Draw(GameTime gameTime)
        {
            //RasterizerState myRasterizerStateWired = new RasterizerState();
            //myRasterizerStateWired.FillMode = FillMode.WireFrame;
            //myRasterizerStateWired.CullMode = CullMode.None;
            //GraphicsDevice.RasterizerState = myRasterizerStateWired;

            //model.Draw(this.world, camera.ViewMatrix, camera.ProjectionMatrix);
            foreach (ModelMesh mesh in this.model.Meshes)
            {
                foreach (BasicEffect effect in mesh.Effects)
                {
                    //effect.World = this.world;
                    effect.World = boneTransforms[mesh.ParentBone.Index] * this.world;
                    effect.View = camera.ViewMatrix;
                    effect.Projection = camera.ProjectionMatrix;
                    effect.EnableDefaultLighting();
                    effect.PreferPerPixelLighting = true;
                    if (CallCustomizeEffect != null) 
                    {
                        CallCustomizeEffect(effect);
                    }

                }
                mesh.Draw();
            }

            if (drawBoundingBox)
            {
                modelBoundingBoxbuffers = CreateBoundingBoxBuffers(modelBoundingBox, Game.GraphicsDevice);

                DrawBoundingBox();
            }

            //RasterizerState myRasterizerStateNormal = new RasterizerState();
            //myRasterizerStateNormal.FillMode = FillMode.Solid;//padrão
            //myRasterizerStateNormal.CullMode = CullMode.CullCounterClockwiseFace;//padrão
            //GraphicsDevice.RasterizerState = myRasterizerStateNormal;
        }

        #endregion

        #region Calcula BoundingBox

        /// <summary>
        /// Codigo do Professor Cleber
        /// </summary>
        /// <returns></returns>
        public virtual BoundingBox CalculateBoundingBox()
        {

            // Create variables to hold min and max xyz values for the model. Initialise them to extremes
            Vector3 modelMax = new Vector3(float.MinValue, float.MinValue, float.MinValue);
            Vector3 modelMin = new Vector3(float.MaxValue, float.MaxValue, float.MaxValue);

            foreach (ModelMesh mesh in model.Meshes)
            {
                //Create variables to hold min and max xyz values for the mesh. Initialise them to extremes
                Vector3 meshMax = new Vector3(float.MinValue, float.MinValue, float.MinValue);
                Vector3 meshMin = new Vector3(float.MaxValue, float.MaxValue, float.MaxValue);

                // There may be multiple parts in a mesh (different materials etc.) so loop through each
                foreach (ModelMeshPart part in mesh.MeshParts)
                {
                    // The stride is how big, in bytes, one vertex is in the vertex buffer
                    // We have to use this as we do not know the make up of the vertex
                    int stride = part.VertexBuffer.VertexDeclaration.VertexStride;

                    byte[] vertexData = new byte[stride * part.NumVertices];
                    part.VertexBuffer.GetData(part.VertexOffset * stride, vertexData, 0, part.NumVertices, 1); // fixed 13/4/11

                    // Find minimum and maximum xyz values for this mesh part
                    // We know the position will always be the first 3 float values of the vertex data
                    Vector3 vertPosition = new Vector3();
                    for (int ndx = 0; ndx < vertexData.Length; ndx += stride)
                    {
                        vertPosition.X = BitConverter.ToSingle(vertexData, ndx);
                        vertPosition.Y = BitConverter.ToSingle(vertexData, ndx + sizeof(float));
                        vertPosition.Z = BitConverter.ToSingle(vertexData, ndx + sizeof(float) * 2);

                        // update our running values from this vertex
                        meshMin = Vector3.Min(meshMin, vertPosition);
                        meshMax = Vector3.Max(meshMax, vertPosition);
                    }
                }

                // transform by mesh bone transforms
                meshMin = Vector3.Transform(meshMin, boneTransforms[mesh.ParentBone.Index] * world);
                meshMax = Vector3.Transform(meshMax, boneTransforms[mesh.ParentBone.Index] * world);

                // Expand model extents by the ones from this mesh
                modelMin = Vector3.Min(modelMin, meshMin);
                modelMax = Vector3.Max(modelMax, meshMax);
            }


            // Create and return the model bounding box
            return new BoundingBox(modelMin, modelMax);

        }

        /// <summary>
        /// Calculo de boundbox USADO por esta classe
        /// </summary>
        /// <param name="model"></param>
        /// <param name="world"></param>
        /// <returns></returns>
        protected static BoundingBox CreateBoundingBox(Model model, Matrix world)
        {
            Matrix[] boneTransforms = new Matrix[model.Bones.Count];
            model.CopyAbsoluteBoneTransformsTo(boneTransforms);

            BoundingBox result = new BoundingBox();
            result.Min = new Vector3(float.MaxValue, float.MaxValue, float.MaxValue);
            result.Max = new Vector3(float.MinValue, float.MinValue, float.MinValue);

            foreach (ModelMesh mesh in model.Meshes)
                foreach (ModelMeshPart meshPart in mesh.MeshParts)
                {
                    BoundingBox? meshPartBoundingBox = GetBoundingBox(meshPart, boneTransforms[mesh.ParentBone.Index] * world);
                    if (meshPartBoundingBox != null)
                        result = BoundingBox.CreateMerged(result, meshPartBoundingBox.Value);
                }

            return result;
        }

        private static BoundingBox? GetBoundingBox(ModelMeshPart meshPart, Matrix transform)
        {
            if (meshPart.VertexBuffer == null)
                return null;

            Vector3[] positions = VertexElementExtractor.GetVertexElement(meshPart, VertexElementUsage.Position);
            if (positions == null)
                return null;

            Vector3[] transformedPositions = new Vector3[positions.Length];
            Vector3.Transform(positions, ref transform, transformedPositions);

            return BoundingBox.CreateFromPoints(transformedPositions);
        }

        protected BoundingBox UpdateBoundingBox(Model model, Matrix worldTransform)
        {
            // Initialize minimum and maximum corners of the bounding box to max and min values
            Vector3 min = new Vector3(float.MaxValue, float.MaxValue, float.MaxValue);
            Vector3 max = new Vector3(float.MinValue, float.MinValue, float.MinValue);

            // For each mesh of the model
            foreach (ModelMesh mesh in model.Meshes)
            {
                foreach (ModelMeshPart meshPart in mesh.MeshParts)
                {
                    // Vertex buffer parameters
                    int vertexStride = meshPart.VertexBuffer.VertexDeclaration.VertexStride;
                    int vertexBufferSize = meshPart.NumVertices * vertexStride;

                    // Get vertex data as float
                    float[] vertexData = new float[vertexBufferSize / sizeof(float)];
                    meshPart.VertexBuffer.GetData<float>(vertexData);

                    // Iterate through vertices (possibly) growing bounding box, all calculations are done in world space
                    for (int i = 0; i < vertexBufferSize / sizeof(float); i += vertexStride / sizeof(float))
                    {
                        Vector3 transformedPosition = Vector3.Transform(new Vector3(vertexData[i], vertexData[i + 1], vertexData[i + 2]), worldTransform);

                        min = Vector3.Min(min, transformedPosition);

                        max = Vector3.Max(max, transformedPosition);

                    }
                }
            }

            // Create and return bounding box
            return new BoundingBox(min, max);
        }

        #endregion

        #region Propriedades da classe
        /// <summary>
        /// Propriedade para alterar a escala do objeto de forma global
        /// </summary>
        public float Scale {
            get 
            {
                return scale;
            }
        
            set
            {
                scale = value;
                scaleX = scaleY = scaleZ = scale;
            }

        }

        /// <summary>
        /// Propriedade angulo de rotação sobre o eixo X em radianos
        /// </summary>
        public float RotationX
        {
            get { return rotationX; }
            set { rotationX = value; }
        }

        /// <summary>
        /// Propriedade angulo de rotação sobre o eixo X em graus
        /// </summary>
        public float RotationXDegrees
        {
            get { return MathHelper.ToDegrees( rotationX); }
            set { rotationX = MathHelper.ToRadians(value); }
        }

        /// <summary>
        /// Propriedade angulo de rotação sobre o eixo Y
        /// </summary>
        public float RotationY
        {
            get { return rotationY; }
            set { rotationY = value; }
        }

        /// <summary>
        /// Propriedade angulo de rotação sobre o eixo Y em graus
        /// </summary>
        public float RotationYDegrees
        {
            get { return MathHelper.ToDegrees(rotationY); }
            set { rotationY = MathHelper.ToRadians(value); }
        }

        /// <summary>
        /// Propriedade angulo de rotação sobre o eixo Y
        /// </summary>
        public float RotationZ
        {
            get { return rotationZ; }
            set { rotationZ = value; }
        }

        /// <summary>
        /// Propriedade angulo de rotação sobre o eixo Z em graus
        /// </summary>
        public float RotationZDegrees
        {
            get { return MathHelper.ToDegrees(rotationZ); }
            set { rotationZ = MathHelper.ToRadians(value); }
        }
   
        /// <summary>
        ///Vetor da posição do objeto
        /// </summary>
        public Vector3 Position 
        {
            get { return position; }
            set 
            { 
                position = value;
                positionX = position.X;
                positionY = position.Y;
                positionZ = position.Z;
             }
        }

        /// <summary>
        /// Posição X
        /// </summary>
        public float PositionX
        {
            get { return positionX; }
            set 
            { 
                positionX = value;
                position.X = positionX;
            }
        }

        /// <summary>
        /// Posição Y
        /// </summary>
        public float PositionY
        {
            get { return positionY; }
            set
            {
                positionY = value;
                position.Y = positionY;
            }
        }

        /// <summary>
        /// Posição Z
        /// </summary>
        public float PositionZ
        {
            get { return positionZ; }
            set
            {
                positionZ = value;
                position.Z = positionZ;
            }
        }

        /// <summary>
        /// Desenha caixa de colisão para testes
        /// </summary>
        public bool FlagDrawBoundingBox { get { return drawBoundingBox; } set { drawBoundingBox = value; } }

        public delegate void CustomizeEffect(BasicEffect effect);

        public CustomizeEffect CallCustomizeEffect;

        #endregion

        #region Drawing BoundBox

        protected BoundingBoxBuffers CreateBoundingBoxBuffers(BoundingBox boundingBox, GraphicsDevice graphicsDevice)
        {
            BoundingBoxBuffers boundingBoxBuffers = new BoundingBoxBuffers();

            boundingBoxBuffers.PrimitiveCount = 24;
            boundingBoxBuffers.VertexCount = 48;

            VertexBuffer vertexBuffer = new VertexBuffer(graphicsDevice,
                typeof(VertexPositionColor), boundingBoxBuffers.VertexCount,
                BufferUsage.WriteOnly);
            List<VertexPositionColor> vertices = new List<VertexPositionColor>();

            const float ratio = 5.0f;

            Vector3 xOffset = new Vector3((boundingBox.Max.X - boundingBox.Min.X) / ratio, 0, 0);
            Vector3 yOffset = new Vector3(0, (boundingBox.Max.Y - boundingBox.Min.Y) / ratio, 0);
            Vector3 zOffset = new Vector3(0, 0, (boundingBox.Max.Z - boundingBox.Min.Z) / ratio);
            Vector3[] corners = boundingBox.GetCorners();

            // Corner 1.
            AddVertex(vertices, corners[0]);
            AddVertex(vertices, corners[0] + xOffset);
            AddVertex(vertices, corners[0]);
            AddVertex(vertices, corners[0] - yOffset);
            AddVertex(vertices, corners[0]);
            AddVertex(vertices, corners[0] - zOffset);

            // Corner 2.
            AddVertex(vertices, corners[1]);
            AddVertex(vertices, corners[1] - xOffset);
            AddVertex(vertices, corners[1]);
            AddVertex(vertices, corners[1] - yOffset);
            AddVertex(vertices, corners[1]);
            AddVertex(vertices, corners[1] - zOffset);

            // Corner 3.
            AddVertex(vertices, corners[2]);
            AddVertex(vertices, corners[2] - xOffset);
            AddVertex(vertices, corners[2]);
            AddVertex(vertices, corners[2] + yOffset);
            AddVertex(vertices, corners[2]);
            AddVertex(vertices, corners[2] - zOffset);

            // Corner 4.
            AddVertex(vertices, corners[3]);
            AddVertex(vertices, corners[3] + xOffset);
            AddVertex(vertices, corners[3]);
            AddVertex(vertices, corners[3] + yOffset);
            AddVertex(vertices, corners[3]);
            AddVertex(vertices, corners[3] - zOffset);

            // Corner 5.
            AddVertex(vertices, corners[4]);
            AddVertex(vertices, corners[4] + xOffset);
            AddVertex(vertices, corners[4]);
            AddVertex(vertices, corners[4] - yOffset);
            AddVertex(vertices, corners[4]);
            AddVertex(vertices, corners[4] + zOffset);

            // Corner 6.
            AddVertex(vertices, corners[5]);
            AddVertex(vertices, corners[5] - xOffset);
            AddVertex(vertices, corners[5]);
            AddVertex(vertices, corners[5] - yOffset);
            AddVertex(vertices, corners[5]);
            AddVertex(vertices, corners[5] + zOffset);

            // Corner 7.
            AddVertex(vertices, corners[6]);
            AddVertex(vertices, corners[6] - xOffset);
            AddVertex(vertices, corners[6]);
            AddVertex(vertices, corners[6] + yOffset);
            AddVertex(vertices, corners[6]);
            AddVertex(vertices, corners[6] + zOffset);

            // Corner 8.
            AddVertex(vertices, corners[7]);
            AddVertex(vertices, corners[7] + xOffset);
            AddVertex(vertices, corners[7]);
            AddVertex(vertices, corners[7] + yOffset);
            AddVertex(vertices, corners[7]);
            AddVertex(vertices, corners[7] + zOffset);

            vertexBuffer.SetData(vertices.ToArray());
            boundingBoxBuffers.Vertices = vertexBuffer;

            IndexBuffer indexBuffer = new IndexBuffer(graphicsDevice, IndexElementSize.SixteenBits, boundingBoxBuffers.VertexCount,
                BufferUsage.WriteOnly);
            indexBuffer.SetData(Enumerable.Range(0, boundingBoxBuffers.VertexCount).Select(i => (short)i).ToArray());
            boundingBoxBuffers.Indices = indexBuffer;

            return boundingBoxBuffers;
        }

        private static void AddVertex(List<VertexPositionColor> vertices, Vector3 position)
        {
            vertices.Add(new VertexPositionColor(position, Color.White));
        }

        protected void DrawBoundingBox()
        {
            Game.GraphicsDevice.SetVertexBuffer(modelBoundingBoxbuffers.Vertices);
            Game.GraphicsDevice.Indices = modelBoundingBoxbuffers.Indices;

            lineEffect.World = Matrix.Identity;
            lineEffect.View = camera.ViewMatrix;
            lineEffect.Projection = camera.ProjectionMatrix;

            foreach (EffectPass pass in lineEffect.CurrentTechnique.Passes)
            {
                pass.Apply();
                Game.GraphicsDevice.DrawIndexedPrimitives(PrimitiveType.LineList, 0, 0,
                    modelBoundingBoxbuffers.VertexCount, 0, modelBoundingBoxbuffers.PrimitiveCount);
            }
        }

        #endregion

    }

    #region classes auxiliares
    public class BoundingBoxBuffers
    {
        public VertexBuffer Vertices;
        public int VertexCount;
        public IndexBuffer Indices;
        public int PrimitiveCount;
    }

    public static class VertexElementExtractor
    {
        public static Vector3[] GetVertexElement(ModelMeshPart meshPart, VertexElementUsage usage)
        {
            VertexDeclaration vd = meshPart.VertexBuffer.VertexDeclaration;
            VertexElement[] elements = vd.GetVertexElements();

            Func<VertexElement, bool> elementPredicate = ve => ve.VertexElementUsage == usage && ve.VertexElementFormat == VertexElementFormat.Vector3;
            if (!elements.Any(elementPredicate))
                return null;

            VertexElement element = elements.First(elementPredicate);

            Vector3[] vertexData = new Vector3[meshPart.NumVertices];
            meshPart.VertexBuffer.GetData((meshPart.VertexOffset * vd.VertexStride) + element.Offset,
                vertexData, 0, vertexData.Length, vd.VertexStride);

            return vertexData;
        }
    }
#endregion
}


